package gazzera.node.core.plugins.download;

import gazzera.gui.*;
import gazzera.gui.messages.*;
import gazzera.node.core.*;
import gazzera.node.core.messages.*;
import gazzera.node.core.peer.*;
import gazzera.node.core.plugins.shareplugin.*;
import gazzera.utils.*;

import java.io.*;
import java.util.*;


public class DownloadPlugin implements Plugin
{
	private ICore core = null;
	private Downloads downloads = null;
	
	private Map<Node, Download> downloaders = new HashMap<Node, Download>();
	private Set<VerificationTask> verificationTasks = new HashSet<VerificationTask>();
	
	private File partial;
	private File completed;
	
	
    @Override
    public void command(GuiMessage msg) throws Exception
    {
		if (msg instanceof NewDownload)
		{
		    NewDownload cmd = (NewDownload)msg;
		    startNewDownload(cmd);
		}
		else if (msg instanceof CancelDownload)
		{
		    CancelDownload c = (CancelDownload)msg;
			cancelDownload(c);
		}
		else if(msg instanceof Status)
		{
			for (Download d : downloads)
			{
				updateDownload(core.getGui(), d);
			}
		}
    }
    private void cancelDownload(CancelDownload c) throws IOException
    {
		Download d = downloads.getById(c.id);
		if(d==null) return;
		for (VerificationTask t : verificationTasks)
		{
			if(t.fileId.equals(d.fileID))
			{
				t.interrupt();
			}
		}
		
		// tell the node to stop sending!
		// and remove them from downloaders
		List<Node> dwnldrs = new ArrayList<Node>(downloaders.keySet());
		for (Node n : dwnldrs)
		{
		    if(downloaders.get(n) == d)
		    {
				n.enqueue(new NotInterested(d.fileID));
				downloaders.remove(n);
		    }
		}
		
		
		d.terminate(false, null);
		
		downloads.remove(d);
		core.getGui().pass(new RemoveDownload(d.id, false));
	
    }
    
    
	public File getCompletedFileFolder()
	{
		return completed;
	}

    private void startNewDownload(NewDownload cmd)
    {
    	// controls to see if we can start this download
    	if(downloads.isDownloading(cmd.fileId))
    	{
    		core.getGui().pass(new Log("Already downloading"));
    		return;
    	}
    	
    	try
		{
    	    		Download d = new Download(cmd.fileId, cmd.name, cmd.size, partial, "subfolder");
			downloads.add(d);
			
			updateDownload(core.getGui(), d);
			if(d.fileID instanceof Hash)
			{
			    ChunkSearch cs = new ChunkSearch(d.fileID, d.size);
			    core.broadcast(cs);
			}
			else if(d.fileID instanceof UserFileID)
			{
			    // we only know user and path
			    Node owner = core.find(((UserFileID)d.fileID).getUserHash());
			    // do we have the owner connected ?
			    if(owner!= null)
			    {
				// ask for his file
				ChunkSearch cs = new ChunkSearch(d.fileID, d.size);
				owner.enqueue(cs);
			    }
			    
			}
		}
		catch (IOException e)
		{
			core.logErr(e);
			core.getGui().pass(new Log("can't start download: "+e.getMessage()));
		}
    	
    }

    @Override
    public void dataPumpTerminated(Node node, DataPumpSource src) throws Exception
    {
	
    }

    @Override
    public void finish() throws Exception
    {
    	downloads.save();
    	for (Download d : downloads)
		{
			d.save();
		}
    }
    
    @Override
    public void init(ICore core) throws Exception
    {
    	this.core = core;
    	
	partial = new File(core.getConfig().getMainFolder(), "partial");
	partial.mkdirs();
	completed = new File(core.getConfig().getMainFolder(), "completed");
	completed.mkdirs();

    	
    	downloads = new Downloads(core.getConfig());
    	downloads.load();    	
    }

    @Override
    public void message(Node node, Message msg) throws Exception
    {
    	if(msg instanceof ChunksAvaiable)
    	{
    		ChunksAvaiable ca = (ChunksAvaiable)msg;
    		replyToChunkAvaiable(node, ca);
    	}
    	else if(msg instanceof ReadyToSend)
    	{
    		ReadyToSend ca = (ReadyToSend)msg;
    		replyToReadyToSend(node, ca);
    	}
    	else if(msg instanceof ChunkData)
    	{
			ChunkData ca = (ChunkData) msg;
			data(node, ca);
    	}
    	else if(msg instanceof FileHash)
    	{
    		FileHash ca = (FileHash) msg;
			replyToFileHash(node, ca);
    	}
    }
	private void replyToFileHash(Node node, FileHash ca) throws IOException 
	{
		// we are notified that i file we only know by its UserFileID got hashed and we can now promote to hash
		if(!node.getHash().equals(ca.userFileId.getUserHash()))
		{
			throw new RuntimeException("Receiving FileHash from wrong person "+node.getHash()+" "+ca.userFileId.getUserHash());
		}
		
		Download d = downloads.get(ca.userFileId);
		if(d!=null)
		{
			if(downloaders.containsValue(d))
			{
				// this should never happen as we can only be downloading this from the user sharing it, and it reply only on enqueued
				throw new RuntimeException("Receiving FileHash but the file is being downloaded right now!! "+ca.userFileId.getFilePath()+" "+ca.userFileId.getUserHash() );
			}
			d.fileID = ca.hash;
			d.save();
		}
		else
		{
			System.out.println("FileHash for a file we aren't downloading");
		}
		
		
	}
	private void data(Node node, ChunkData ca) throws Exception, IOException
	{
		Download d = downloaders.get(node);
		if( (d != null) && (d.phase == DownloadStatus.DOWNLOADING))
		{
			boolean chunkTerminated = d.writeData(node, ca.data);
			
			long millis = System.currentTimeMillis();
			if((millis-d.lastNotifiedTime)>1000)
			{
				d.tick(millis);
				updateDownload(core.getGui(), d);
			}
			
			if (chunkTerminated)
			{
				d.chunkDownloadTerminated(node);
				downloaders.remove(node);
				
				d.speed = 0;
				updateDownload(core.getGui(), d);
				
				if (d.isCompleted())
				{
					downloadCompleted(d);
				}
				else
				{
					// search some more chunks
					node.enqueue(new Enqueue(d.fileID));
				}
			}
		} else
		{
			System.out.println("ChunkData without download!!");
		}
	}
    
    private void verificationCompleted(Download d, boolean valid, Hash calculatedHash) throws IOException
    {
    	if(valid)
    	{
    		File completedFile = d.terminate(true, completed);
    		downloads.remove(d);
    		
    		if(completedFile!=null)
    		{
    			// put on HashDatabase
    			SharePlugin sp = core.getPlugin(SharePlugin.class);
    			sp.addHashedFile(calculatedHash, completedFile);
    		}
    		
    		core.getGui().pass(new RemoveDownload(d.id, true));
    	}
    	else
    	{
    	    System.out.println("HASH INVALID!! "+d.name);
    	    core.getGui().pass(new Log("HASH INVALID!! "+d.name));
    	}
    }
    
	private void downloadCompleted(Download d) throws IOException
	{
		// download completed, start verification
		d.phase = DownloadStatus.VERIFYING;
		VerificationTask t = new VerificationTask(d.fileID, d.getFile());
		core.startTask(t);
		verificationTasks.add(t);
	}

	private void replyToReadyToSend(Node node, ReadyToSend msg) throws Exception
	{
	    
	    if(downloaders.containsKey(node))
	    {
			// 1 download per user
			core.getGui().pass(new Log("Node ready to send but is already in downloaders! "+node));
			return;
	    }
	    
		Download d = downloads.get(msg.fileId);
		if( (d != null) && (d.phase == DownloadStatus.DOWNLOADING))
		{
			// TODO check if avaiable bitset is ok!
				Pair<Integer, Integer> p = d.getChunkToStart(msg.avaiable);
				if (p == null)
				{
					// nothing needed in avaiable
					node.enqueue(new NotInterested(msg.fileId));
				}
				else
				{
					// let's prepare to receive!!
					int chunkIdx = p.a;
					int offset = p.b;
					d.startChunk(node, chunkIdx, offset);
					node.enqueue(new StartToSend(msg.fileId, chunkIdx, offset));
					downloaders.put(node, d);
				}

		} else
		{
			node.enqueue(new NotInterested(msg.fileId));
		}

	}
    
	private void replyToChunkAvaiable(Node node, ChunksAvaiable ca)
	{
		Download d = downloads.get(ca.fileId);
		if( (d != null) && (d.phase == DownloadStatus.DOWNLOADING))
		{
			// TODO check if avaiable bitset is ok!
			if(ca.avaiable.equals(new BitSet(0)))
			{
				// let's enqueue!
				node.enqueue(new Enqueue(d.fileID));
			}
		}
		
	}

    @Override
    public void nodeConnected(Node node) throws Exception
    {
    	for (Download d : downloads)
    	{
    	    if(d.phase == DownloadStatus.DOWNLOADING)
    	    {
    	    	boolean request = false;
    	    	if (d.fileID instanceof Hash) 
    	    	{
    	    		request = true;
				}
    	    	else if (d.fileID instanceof UserFileID)
    	    	{
    	    		// if it's a UserFileID we request only if the connected user is the owner of the file
    	    		request = ((UserFileID)d.fileID).getUserHash().equals(node.getHash());
    	    	}
    	    	
    	    	if(request)
    	    	{
    	    		ChunkSearch cs = new ChunkSearch(d.fileID, d.size);
    	    		node.enqueue(cs);
    	    	}
    	    }
    	}
    }

    @Override
    public void nodeDisconnected(Node node) throws Exception
    {
    	if(downloaders.containsKey(node))
    	{
    		Download d = downloaders.get(node);
    		d.chunkDownloadTerminated(node);
    		d.speed = 0;
    		updateDownload(core.getGui(), d);
    		if(d.isCompleted())
    		{
    		    // TODO ?????
    		    core.getGui().pass(new Log("Download completo!!"));
    		}
    		downloaders.remove(node);
    	}
    }

    @Override
    public void start() throws Exception
    {
    	
    	for (Download d : downloads)
		{
    		if (d.isCompleted())
			{
				downloadCompleted(d);
			}
		}
		
    }

    @Override
    public void taskTerminated(Task task, boolean interrupted) throws Exception
    {
    	if (!interrupted)
    	{
    		if(task instanceof VerificationTask)
    		{
    			VerificationTask t = (VerificationTask)task;
    			verificationTasks.remove(t);
    			Download d = downloads.get(t.fileId);
    			if(d!=null)
    			{
    				verificationCompleted(d, t.valid, t.calculatedHash);
    			}
    		}
    	}
    }
    
    public static void updateDownload(GuiInterface gui, Download d)
    {
    	DownloadUpdate m = new DownloadUpdate(d.id, d.fileID, d.name, d.size, d.downloaded, d.speed);
    	gui.pass(m);
    }
    

	
}
